package com.boot2.core.aop;

import com.boot2.core.HlAssert;
import com.boot2.core.constant.CoreConstant;
import com.boot2.core.constant.I18nMessageContantKey;
import com.boot2.core.dict.ErrorCode;
import com.boot2.core.exception.ParamEmptyException;
import com.boot2.core.model.AbstractLog;
import com.boot2.core.model.UserLog;
import com.boot2.core.service.AopService;
import com.boot2.core.utils.FileUtil;
import com.boot2.core.utils.ResultBuilder;
import com.boot2.core.utils.ServletUtil;
import com.boot2.core.utils.StringUtils;
import com.boot2.core.model.vo.Result;
import com.boot2.core.web.RequestContext;
import com.boot2.core.web.validator.FileUpload;
import com.boot2.core.web.validator.ParamRequired;
import lombok.Data;
import lombok.extern.apachecommons.CommonsLog;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.hibernate.validator.internal.engine.path.PathImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import javax.validation.executable.ExecutableValidator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;

/**
 * @author zhangweilin
 * @date 2018/6/9 18:08
 * @description: 校验web参数的aop工具类, 如果使用自带的校验，读取优先级如下:NotBlank.user.phone→phone→user.phone
 */
@CommonsLog
@Data
public abstract class AbstractAop {

    public static final String methodJoinPointKey = "_methodJoinPoint";
    private final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    private final ExecutableValidator validator = factory.getValidator().forExecutables();
    @Autowired
    MessageSource messageSource;
    @Autowired
    AopService aopService;
    /**
     * 默认为内置的UserLog,可以通过enableUserLog(xx.class)来改变
     */
    Class<? extends AbstractLog> logModelClass = UserLog.class;

    private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
    /**
     //     * 日志对应的model类
     //     */
//    private Class<? extends  AbstractLog> logModelClazz;

    /**
     * 定义有一个切入点，范围为web包下的类
     */
//    @Pointcut("execution(public * com..*.controller..*.*(..))")
    public void checkParam() {
    }

    //    @Before("checkParam()")
    public void doBefore(JoinPoint joinPoint) {

    }

    /**
     * 开启日志记录功能
     *
     * @param logModelClazz
     */
    public void enableUserLog(Class<? extends AbstractLog> logModelClass) {
        this.logModelClass = logModelClass;
    }

    /**
     * 检查参数是否为空
     */
//    @Around("checkParam()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        HttpServletRequest request = RequestContext.getRequest();
        if (null != logModelClass) {
            AbstractLog abstractLog = logModelClass.newInstance();
            request.setAttribute(CoreConstant.userLog, abstractLog);
        }
        request.setAttribute(methodJoinPointKey, pjp);
//        System.out.println("ParamCheckAop.doAround");

        //取参数，如果没参数，那肯定不校验了
        Object[] objects = pjp.getArgs();
        if (objects.length == 0) {
            return proceed(pjp);
        }

        List<String> errorMessageList = new ArrayList<>();
        /**************************校验封装好的javabean**********************/
        //寻找带BindingResult参数的方法，然后判断是否有error，如果有则是校验不通过
        for (Object object : objects) {
            if (object instanceof BeanPropertyBindingResult) {
                //有校验
                BeanPropertyBindingResult result = (BeanPropertyBindingResult) object;
                if (result.hasErrors()) {
                    List<ObjectError> list = result.getAllErrors();
                    for (ObjectError error : list) {
                        String defaultMessage = error.getDefaultMessage();
//                        System.out.println("defaultMessage = " + defaultMessage);
                        //得到的是形如 NotBlank.users.password的字符串
                        String[] codes = error.getCodes();
//                        System.out.println("ArrayUtils.toString(codes) = " + ArrayUtils.toString(codes));
                        String code = codes[0];
                        String i18nMessage = null;
                        //如果没有自定义消息key,则用系统默认自带的
                        try {
                            i18nMessage = getI18nMessage(code, null);
//                            System.out.println("i18nMessage1 = " + i18nMessage);
                        } catch (Exception e) {
                            log.warn("获取国际化消息失败");

                            //此时从NotBlank.user.phone中读取到user.phone,在读取国际化配置时，读取优先级如下:NotBlank.user.phone→phone→user.phone，而返回给客户端的只显示phone
                            //先读取要展现给客户端的值
                            //得到形如phone
                            String codeLast = StringUtils.getLastSection(code, ".");
                            try {
                                codeLast = getI18nMessage(codeLast, null);
                            } catch (Exception e1) {
                                //如果没有读取到phone对应的值，则直接显示为phone
                            }

                            //读取user.phone
                            String code1 = StringUtils.getBySplitIndex(code, ".", 1);
//                            System.out.println("code = " + code);
                            try {
                                i18nMessage = getI18nMessage(code1, null);
                                i18nMessage = "【" + i18nMessage + "】" + defaultMessage;
//                                System.out.println("i18nMessage2 = " + i18nMessage);
                            } catch (Exception e1) {
                                log.warn("获取国际化消息还是失败【字段名为消息key】");
                                //如果读取字段名还是失败，说明只能直接将字段名显示到消息体中了
                                i18nMessage = "【" + codeLast + "】" + defaultMessage;
//                                System.out.println("i18nMessage3 = " + i18nMessage);
                            }
                        }
                        errorMessageList.add(i18nMessage);
                    }
                    if (CollectionUtils.isNotEmpty(errorMessageList)) {
                        Result<Object> response = null;
                        if (ServletUtil.isAjaxRequest()) {
                            if (errorMessageList.size() == 1) {
                                response = ResultBuilder.buildResult(ErrorCode.code_400, errorMessageList.get(0));
                            } else {
                                response = ResultBuilder.buildResult(ErrorCode.code_400, errorMessageList);
                            }
                            return response;
                        } else {
                            request.setAttribute(CoreConstant.operationToastError, errorMessageList);
                            return proceed(pjp);
                        }
                    }
                }
            }
        }

        /************校验自定义注释********/
        MethodSignature signature = ((MethodSignature) pjp.getSignature());
        //得到拦截的方法
        Method method = signature.getMethod();
        FileUpload fileUpload = method.getAnnotation(FileUpload.class);

        //获取方法参数注解，返回二维数组是因为某些参数可能存在多个注解
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        if (parameterAnnotations == null || parameterAnnotations.length == 0) {
            return proceed(pjp);
        }
        //获取方法参数名
        String[] paramNames = signature.getParameterNames();
        //获取参数值
        Object[] paramValues = pjp.getArgs();
        //获取方法参数类型
        Class<?>[] parameterTypes = method.getParameterTypes();
        for (int i = 0; i < parameterAnnotations.length; i++) {
            //处理文件上传
            handFileUpload(errorMessageList, fileUpload, paramValues, parameterTypes, i);

            for (int j = 0; j < parameterAnnotations[i].length; j++) {
                Annotation annotation = parameterAnnotations[i][j];
                if (null != annotation) {
                    handParamRequired(errorMessageList, paramNames, paramValues, parameterTypes, i, annotation);
                }
            }
        }

        /**************************用spring自带的注解，校验普通参数*************************/
        handValid(pjp, objects, errorMessageList, method);
        if (CollectionUtils.isNotEmpty(errorMessageList)) {
            if (ServletUtil.isAjaxRequest()) {
                Result<Object> result = null;
                if (errorMessageList.size() == 1) {
                    result = ResultBuilder.buildResult(ErrorCode.code_400, errorMessageList.get(0));
                } else {
                    result = ResultBuilder.buildResult(ErrorCode.code_400, errorMessageList);
                }
                return result;
            } else {
                request.setAttribute(CoreConstant.operationToastError, errorMessageList);
                request.getRequestDispatcher("/error").forward(request, RequestContext.getResponse());
                return null;
//                return proceed(pjp);
            }
        }
        return proceed(pjp);
    }

    private Object proceed(ProceedingJoinPoint pjp) throws Throwable {
        Object proceed = pjp.proceed();
        HttpServletRequest request = RequestContext.getRequest();
        aopService.handLog(RequestContext.getHandler(), request, pjp);
        return proceed;
    }


    /**
     * 用spring自带的注解，校验普通参数
     *
     * @param pjp
     * @param objects
     * @param errorMessageList
     * @param method
     */
    private void handValid(ProceedingJoinPoint pjp, Object[] objects, List<String> errorMessageList, Method method) {
        //  获得切入目标对象
        Object target = pjp.getThis();
        // 获得切入的方法
        // 执行校验，获得校验结果
        Set<ConstraintViolation<Object>> validResult = validMethodParams(target, method, objects);
//        JSONObject jsonObject = new JSONObject();
        //如果有校验不通过的
        if (!validResult.isEmpty()) {
            // 获得方法的参数名称
            String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
            for (ConstraintViolation<Object> constraintViolation : validResult) {
                // 获得校验的参数路径信息
                PathImpl pathImpl = (PathImpl) constraintViolation.getPropertyPath();
                // 获得校验的参数位置
                int paramIndex = pathImpl.getLeafNode().getParameterIndex();
                // 获得校验的参数名称
                String paramName = parameterNames[paramIndex];

                //貌似此已经被内置国际化
                String message = constraintViolation.getMessage();
                String i18nParamName = getI18nMessage(paramName, null);

                //将错误信息一次性全输出
                errorMessageList.add(i18nParamName + message);
            }
        }
    }

    /**
     * 处理文件上传
     *
     * @param errorMessageList
     * @param fileUpload
     * @param paramValues
     * @param parameterTypes
     * @param i
     */
    private void handFileUpload(List<String> errorMessageList, FileUpload fileUpload, Object[] paramValues, Class<?>[] parameterTypes, int i) {
        if (fileUpload != null) {

            int maxNum = fileUpload.maxNum();
            boolean originalName = fileUpload.originalName();
            RequestContext.getRequest().setAttribute(CoreConstant.uploadOriginalName, originalName);
            Class<?> parameterType = parameterTypes[i];
            if (parameterType.getName().equals(MultipartFile[].class.getName())) {
                MultipartFile[] multipartFileArr = (MultipartFile[]) paramValues[i];
//                如果上传文件数量大于最高限制数量
                if (multipartFileArr.length > maxNum) {
                    String msg = getI18nMessage(I18nMessageContantKey.param_upload_num_error, new String[]{maxNum + "", multipartFileArr.length + ""});
                    errorMessageList.add(msg);
                }
                for (int k = 0; k < multipartFileArr.length; k++) {
                    MultipartFile multipartFile = multipartFileArr[k];
                    handMultipartFile(errorMessageList, fileUpload, multipartFile);
                }
            } else if (parameterType.getName().equals(MultipartFile.class.getName())) {
                MultipartFile multipartFile = (MultipartFile) paramValues[i];
                handMultipartFile(errorMessageList, fileUpload, multipartFile);
            }
        }
    }

    private void handMultipartFile(List<String> errorMessageList, FileUpload fileUpload, MultipartFile multipartFile) {
        long maxSize = fileUpload.maxSize();
        String suffixs = fileUpload.suffixs();
        long size = multipartFile.getSize();
        String name = multipartFile.getName();
        String originalFilename = multipartFile.getOriginalFilename();
        //校验文件大小
        if (size > maxSize) {
            String msg = getI18nMessage(I18nMessageContantKey.param_upload_size_error, new String[]{originalFilename, maxSize + "", size + ""});
            errorMessageList.add(msg);
//                                errorMessageList.add("文件【"+originalFilename+"】错误，每个文件最大不能超过【" + maxSize + "】字节,实际收到【"+size+"】字节");
        } else if (size == 0) {
            //如果为0.说明没有上传文件，此时忽略
            return;
        }

        String suffix = "";
        // 如果有后缀
        if (originalFilename.contains(".")) {
            suffix = FileUtil.getSuffix(originalFilename).substring(1);
        } else {
            suffix = originalFilename.substring(1);
        }
        suffixs = suffixs == null ? "" : suffixs.toLowerCase();
        suffix = suffix == null ? "" : suffix.toLowerCase();
        String[] suffixArr = suffixs.split(",");
        //如果可支持的后缀列表中，包括当前的，则允许上传，否则不允许上传
        if (!ArrayUtils.contains(suffixArr, suffix)) {
            String msg = getI18nMessage(I18nMessageContantKey.param_upload_suffix_error, new String[]{originalFilename, suffixs, suffix});
//                                errorMessageList.add("文件【"+originalFilename+"】错误，只允许上传【" + suffixs + "】格式，实际接到【" + suffix + "】格式");
            errorMessageList.add(msg);
        }
    }

    private void handParamRequired(List<String> errorMessageList, String[] paramNames, Object[] paranValues, Class<?>[] parameterTypes, int i, Annotation annotation) {
        //如果该参数前面的注解是ParamCheck的实例，并且notNull()=true,则进行非空校验
        if (annotation instanceof ParamRequired && ((ParamRequired) annotation).notNull()) {
            try {
                checkParamIsEmpty(paramNames[i], paranValues[i], parameterTypes[i] == null ? null : parameterTypes[i].getName());
            } catch (ParamEmptyException e) {
                errorMessageList.add(e.getMessage());
            }
//                    break;
        }
    }

    /**
     * 在切入点return内容之后切入内容（可以用来对处理返回值做一些加工处理）
     *
     * @param joinPoint
     */
//    @AfterReturning("checkParam()")
    public void doAfterReturning(JoinPoint joinPoint) {
    }

    /**
     * 参数非空校验，如果参数为空，则抛出ParamIsNullException异常
     *
     * @param paramName
     * @param value
     * @param parameterType
     */
    private void checkParamIsEmpty(String paramName, Object value, String parameterType) {
        if (value == null || "".equals(value.toString().trim())) {
            String name = getI18nMessage(paramName, null);
            String msg = getI18nMessage(I18nMessageContantKey.message_empty, new String[]{name});
//            System.out.println("name = " + name);
//            System.out.println("msg = " + msg);
            throw new ParamEmptyException(msg);
        }
    }


    /**
     * 国际化，注意，此只能从message_xx.properties文件中读取国际化信息,而系统自带的校验框架,读取国际化信息只能从
     * ValidationMessages_xx.properties中读取
     *
     * @param messageKey
     * @return
     */
    public String getI18nMessage(String messageKey, Object[] params) {
        String message = "";
        try {
            Locale locale = LocaleContextHolder.getLocale();
            message = messageSource.getMessage(messageKey, params, locale);
        } catch (Exception e) {
//            e.printStackTrace();
//            throw new BusinessException(messageKey + " is invalid.", e);
            HlAssert.isTrue(false, messageKey + " is invalid.");
        }
        return message;
    }

    private <T> Set<ConstraintViolation<T>> validMethodParams(T obj, Method method, Object[] params) {
        return validator.validateParameters(obj, method, params);
    }
}